﻿namespace NTLib.Core.Notifier
{
    using NTLib.Core.Threading;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Collections;
    using System.Runtime.CompilerServices;
    using System.Linq.Expressions;
    using NTLib.Core.Memory;
    using System.Runtime.Serialization;


    /// <summary>
    /// Base Implementation of the interface INotifyPropertyChanged and INotifyDataErrorInfo
    /// </summary>
    /// <typeparam name="Locker">The type of the locker class that could be used. This class will secure the access to the error info.</typeparam>
    [DataContract]
    public abstract class BaseNotifier<Locker> : Disposable, INotifyPropertyChanged, INotifyDataErrorInfo
        where Locker : ILocker, new()
    {
        #region Fields

        private Locker _locker;
        private Dictionary<string, IEnumerable> _errors;

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes a new instance of the <see cref="BaseNotifier{Locker}"/> class.
        /// </summary>
        public BaseNotifier()
        {
            Initialize();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets a value indicating whether this instance has errors.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance has errors; otherwise, <c>false</c>.
        /// </value>
        [IgnoreDataMember]
        public bool HasErrors
        {
            get
            {
                Initialize();
                base.CheckDisposed();
                using (this._locker.ScopeLock(true))
                {
                    return this._errors.Count > 0;
                }
            }
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when the property changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Occurs when an errors is set or clean.
        /// </summary>
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        #endregion

        #region Methods

        #region NotifyDataErrorInfo

        /// <summary>
        /// Gets the errors.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public IEnumerable GetErrors<U>(Expression<Func<U>> propertyName)
        {
            base.CheckDisposed();
            return GetErrors((propertyName.Body as MemberExpression).Member.Name);
        }

        /// <summary>
        /// Gets the errors.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        public IEnumerable GetErrors(string propertyName)
        {
            base.CheckDisposed();
            Initialize();
            using (this._locker.ScopeLock(true))
            {
                IEnumerable errors = null;
                this._errors.TryGetValue(propertyName, out errors);
                return errors;
            }
        }


        /// <summary>
        /// Sets the error with a property name as key.
        /// </summary>
        protected void SetError<T, U>(Expression<Func<U>> propName, params T[] errors)
        {
            base.CheckDisposed();
            SetError(errors, (propName.Body as MemberExpression).Member.Name);
        }

        /// <summary>
        /// Sets the error with a property name as key.
        /// </summary>
        protected void SetError<T>(string propName, params T[] errors)
        {
            base.CheckDisposed();
            SetError(errors, propName);
        }

        /// <summary>
        /// Sets the error with a property name as key.
        /// </summary>
        protected void SetError<T>(IEnumerable<T> errors, [CallerMemberName]string propName = null)
        {
            base.CheckDisposed();
            Initialize();
            using (this._locker.ScopeLock())
            {
                if (!this._errors.ContainsKey(propName))
                    this._errors.Add(propName, errors);
                else
                    this._errors[propName] = this._errors[propName].OfType<object>().Concat(errors.Cast<object>());

                OnErrorChanged(propName);
            }
        }


        /// <summary>
        /// Clears the errors.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected void ClearErrors<U>(Expression<Func<U>> propName)
        {
            base.CheckDisposed();
            ClearErrors((propName.Body as MemberExpression).Member.Name);
        }

        /// <summary>
        /// Clears the errors.
        /// </summary>
        protected void ClearErrors([CallerMemberName]string propName = null)
        {
            base.CheckDisposed();
            Initialize();
            using (this._locker.ScopeLock())
            {
                if (this._errors.ContainsKey(propName))
                    this._errors.Remove(propName);
            }
        }

        /// <summary>
        /// Called when an error is assign to a property.
        /// </summary>
        protected void OnErrorChanged([CallerMemberName]string propName = null)
        {
            base.CheckDisposed();
            if (this.ErrorsChanged != null)
                this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propName));
        }

        #endregion

        #region NotifyPropertyChanged

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <returns><c>true</c> if the variable have been assigned by the value; otherwise <c>false</c></returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected bool SetValue<T>(ref T variable, T value, Expression<Func<T>> lambdaProp)
        {
            base.CheckDisposed();
            return SetValue<T>(ref variable, value, (lambdaProp.Body as MemberExpression).Member.Name);
        }

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <returns><c>true</c> if the variable have been assigned by the value; otherwise <c>false</c></returns>
        protected bool SetValue<T>(ref T variable, T value, [CallerMemberName]string propName = null)
        {
            base.CheckDisposed();
            if (!object.ReferenceEquals(variable, value) &&
                !object.Equals(variable, value))
            {
                variable = value;
                OnPropertyChanged(propName);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <returns><c>true</c> if the variable have been assigned by the value; otherwise <c>false</c></returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        protected bool SetValue<T>(T variable, T value, Action<T> assign, Expression<Func<T>> lambdaProp)
        {
            base.CheckDisposed();
            return SetValue<T>(variable, value, assign, (lambdaProp.Body as MemberExpression).Member.Name);
        }

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <returns><c>true</c> if the variable have been assigned by the value; otherwise <c>false</c></returns>
        protected bool SetValue<T>(T variable, T value, Action<T> assign, [CallerMemberName]string propName = null)
        {
            base.CheckDisposed();
            if (!object.ReferenceEquals(variable, value) &&
                !object.Equals(variable, value))
            {
                assign(value);
                OnPropertyChanged(propName);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Called to notify that a property value have changed.
        /// </summary>
        /// <param name="propName">Name of the property.</param>
        protected void OnPropertyChanged([CallerMemberName]string propName = null)
        {
            base.CheckDisposed();
            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }

        #endregion


        /// <summary>
        /// Dispose methods called whenever is the dipose origin.
        /// </summary>
        protected override void DisposeEveryTime()
        {
            if (this._locker != null)
                this._locker.Dispose();
            base.DisposeEveryTime();
        }

        /// <summary>
        /// Called when deserialize.
        /// </summary>
        /// <param name="ctx">The CTX.</param>
        private void Initialize()
        {
            if (this._locker == null)
            {
                this._locker = new Locker();
                this._errors = new Dictionary<string, IEnumerable>();
            }
        }

        #endregion
    }

    /// <summary>
    /// Base Implementation of the interface INotifyPropertyChanged and INotifyDataErrorInfo
    /// </summary>
    [NotThreadSafe]
    [DataContract]
    public class BaseNotifier : BaseNotifier<NoLocker>
    {
    }
}
